using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

namespace Atomic.Libraries.Jobs
{
	[Serializable]
	public abstract class Job
	{
		private JobResources resources;
		private ReadOnlyList<Job> dependencies;
		private JobManager manager;
		private int index;
		[OptionalField(VersionAdded = 2)] private JobSubmitOptionCollection options;

		// The result is loaded independently upon request.
		[NonSerialized] private JobBaseResult innerResult;

		public Job(params Job[] dependencies)
			: this((IEnumerable<Job>)dependencies)
		{
		}

		public Job(IEnumerable<Job> dependencies)
			: this(new JobResources(), dependencies)
		{
		}
		
		public Job(JobSubmitOptionCollection options, params Job[] dependencies)
			: this(options, (IEnumerable<Job>)dependencies)
		{
		}

		public Job(JobSubmitOptionCollection options, IEnumerable<Job> dependencies)
		{
			this.options = options;
			this.dependencies = new ReadOnlyList<Job>(dependencies);
		}

		public Job(JobResources resources, params Job[] dependencies)
			: this(resources, (IEnumerable<Job>)dependencies)
		{
		}

		public Job(JobResources resources, IEnumerable<Job> dependencies)
		{
			this.resources = resources;
			this.dependencies = new ReadOnlyList<Job>(dependencies);
		}

		protected abstract void Run();
		
		protected virtual bool TryRun(out JobBaseResult result)
		{
			try
			{
				Run();
				result = new JobBaseResult();
			}
			catch (Exception e)
			{
				result = new JobBaseResult(e);
				return false;
			}

			return true;
		}

		public void Submit(JobManager manager)
		{
			if (this.manager != null)
			{
				throw new Exception("Already submitted.");
			}

			this.manager = manager;

			index = manager.Jobs.IndexOf(this);
			if (index == -1)
			{
				throw new Exception("Unknown job manager.");
			}

			Submit();
		}

		private void Submit()
		{
			// Submit the job using the qsub command.
			string id = PbsSubmit();

			manager.Status.Entries[this].Id = id;
			manager.Status.Entries[this].State = JobState.Waiting;
			manager.Status.Entries[this].StartTime = null;
			manager.Status.Entries[this].EndTime = null;
			manager.Status.Entries[this].Save();
		}

		public void Resubmit()
		{
			Resubmit(false);
		}

		public void Resubmit(bool purge)
		{
			if (manager == null)
			{
				throw new Exception("Job not submitted.");
			}

			if (State == JobState.Waiting || State == JobState.Running || State == JobState.Finished)
			{
				// Ignore if already running (must terminate first) or finished succesfully.
				return;
			}

			if (!CheckResubmitDependencies())
			{
				throw new Exception("Job dependents on failed other jobs.");
			}

			List<Job> dependentJobs = new List<Job>();
			foreach (Job job in manager.Jobs)
			{
				bool include = false;

				if (job.Dependencies.Contains(this))
				{
					include = true;
				}

				foreach (Job dependentJob in dependentJobs)
				{
					if (job.Dependencies.Contains(dependentJob))
					{
						include = true;
					}
				}

				if (!include)
				{
					continue;
				}

				if (job.State == JobState.Waiting || job.State == JobState.Running)
				{
					throw new Exception("A job depending on this job is already waiting or running. Terminate before resubmitting.");
				}

				//Console.WriteLine("INCLUDING: {0}", job.Index); // FIXME
				dependentJobs.Add(job);
			}

			innerResult = null;
			SaveResult();

			// Reset jobs depending on this job.
			foreach (Job job in dependentJobs)
			{
				//Console.WriteLine("RESETTING: {0}", job.Index); // FIXME
				manager.Status.Entries[job].State = JobState.Failed;
				manager.Status.Entries[job].EndTime = DateTime.UtcNow;
				manager.Status.Entries[job].Save();
			}

			if (purge)
			{
				Purge();
			}

			// Seems like it required to delete the output file manually.
			ConsoleOutputFile.Delete();

			Submit();

			// Resubmit jobs depending on this job (if not depending on other failed jobs).
			foreach (Job job in dependentJobs)
			{
				if (job.CheckResubmitDependencies())
				{
					job.Resubmit();
				}
			}
		}

		private bool CheckResubmitDependencies()
		{
			//Console.WriteLine("CHECKING: {0}", index); // FIXME

			foreach (Job job in dependencies)
			{
				if (job.State == JobState.Failed)
				{
					//Console.WriteLine(job.State); // FIXME
					//Console.WriteLine("CHECKING FAILED: {0}", job.index); // FIXME
					return false;
				}

				if (!job.CheckResubmitDependencies())
				{
					return false;
				}
			}

			return true;
		}

		public void Start()
		{
			if (manager == null)
			{
				throw new Exception("Job not submitted.");
			}

			if (State == JobState.Running)
			{
				// Already running.
				return;
			}

			// Mark as running before starting the computation.
			manager.Status.Entries[this].State = JobState.Running;
			manager.Status.Entries[this].StartTime = DateTime.UtcNow;
			manager.Status.Entries[this].Save();

			JobState state;
			if (TryRun(out innerResult))
			{
				state = JobState.Finished;
			}
			else
			{
				state = JobState.Failed;
			}

			try
			{
				// Save result on the file system.
				SaveResult();
			}
			catch
			{
				// Mark job as failed in case of serialization error (or similar).
				state = JobState.Failed;
				throw;
			}
			finally
			{
				// Updating status outside the try-catch block. Want to be able to throw exceptions relating to this update process.
				manager.Status.Entries[this].State = state;
				manager.Status.Entries[this].EndTime = DateTime.UtcNow;
				manager.Status.Entries[this].Save();
			}
		}

		public void Terminate()
		{
			if (manager == null)
			{
				// Not submitted. Just ignore termination.
				return;
			}

			if (State == JobState.Finished || State == JobState.Failed)
			{
				// Not running.
				return;
			}

			PbsDelete();

			// Mark as failed. No exception is stored here.
			manager.Status.Entries[this].State = JobState.Failed;
			manager.Status.Entries[this].EndTime = DateTime.UtcNow;
			manager.Status.Entries[this].Save();
		}

		public void Refresh()
		{
			if (manager == null)
			{
				// Not submitted. Just ignore.
				return;
			}

			if (State == JobState.Finished || State == JobState.Failed)
			{
				// Not running.
				return;
			}

			if (!PbsStatus())
			{
				// Job disappeared from the queue. Mark as failed. No exception is stored here.
				manager.Status.Entries[this].State = JobState.Failed;
				manager.Status.Entries[this].EndTime = DateTime.UtcNow;
				manager.Status.Entries[this].Save();
			}
		}

		/// <summary>
		/// Purges output files of this job, i.e. deletes the assigned directory.
		/// </summary>
		public void Purge()
		{
			if (manager == null)
			{
				// Not submitted. Just ignore.
				return;
			}

			if (OutputDirectory.Exists)
			{
				OutputDirectory.Delete(true);
			}
		}

		private void SaveResult()
		{
			using (FileLock fileLock = AcquireLock())
			{
				if (innerResult != null)
				{
					Serializer.SerializeCompressed(ResultFile, innerResult);
				}
				else
				{
					ResultFile.Delete();
				}
			}
		}

		private void LoadResult()
		{
			if (innerResult != null)
			{
				// Already loaded. No need to reload.
				return;
			}

			if (manager.OutputDirectory == null)
			{
				throw new Exception("Job detacted from the job manager.");
			}

			if (State == JobState.Waiting || State == JobState.Running)
			{
				// Don't even try to unserialize job result in this case.
				throw new Exception("Job hasn't finished.");
			}

			using (FileLock fileLock = AcquireLock())
			{
				innerResult = (JobBaseResult)Serializer.DeserializeCompressed(ResultFile);
			}
		}

		private FileLock AcquireLock()
		{
			return FileLock.Acquire(ResultFile.AddFileExtension(".lock"));
		}

		protected void HandleException()
		{
			LoadResult();

			if (innerResult.InnerException != null)
			{
				// Make the usage of jobs fail-safe. No need to check for errors explicitly.
				throw new JobException(this, innerResult.InnerException);
			}

			if (State == JobState.Failed)
			{
				// Also fail if failed for unspecified reasons (e.g. job was deleted).
				throw new JobException(this, "Job failed for unspecified reasons.");
			}
		}

		private string PbsSubmit()
		{
			Process p = new Process();
			p.StartInfo.FileName = "qsub";
			p.StartInfo.UseShellExecute = false;
			p.StartInfo.RedirectStandardInput = true;
			p.StartInfo.RedirectStandardOutput = true;
			p.StartInfo.RedirectStandardError = true;

			p.Start();

			p.StandardInput.WriteLine("#!/bin/bash");
			p.StandardInput.WriteLine("#PBS -o {0}", ConsoleOutputFile.FullName);
			p.StandardInput.WriteLine("#PBS -j oe");
			if (manager.Name != null)
			{
				p.StandardInput.WriteLine("#PBS -N {0}.{1}", manager.Name, index);
			}

			// Read job submission resources from the property in case it has been overridden.
			IEnumerable<IJobSubmitOption> options = SubmitOptions.Options;

			QueueOption queue = (QueueOption)options.Where(o => o is QueueOption).LastOrDefault();
			if (queue != null)
			{
				p.StandardInput.WriteLine("#PBS -q {0}", queue.Queue);
			}

			NodesOption nodes = (NodesOption)options.Where(o => o is NodesOption).LastOrDefault();
			if (nodes != null)
			{
				if (nodes.ProcessorsPerNode == 1)
				{
					p.StandardInput.WriteLine("#PBS -l nodes={0}", nodes.Nodes);
				}
				else
				{
					p.StandardInput.WriteLine("#PBS -l nodes={0}:ppn={1}", nodes.Nodes, nodes.ProcessorsPerNode);
				}
			}

			TimeOption time = (TimeOption)options.Where(o => o is TimeOption).LastOrDefault();
			if (time != null)
			{
				p.StandardInput.WriteLine("#PBS -l walltime={0}:{1:d2}:{2:d2}", time.Time.Hours + 24 * time.Time.Days, time.Time.Minutes, time.Time.Seconds);
			}

			MemoryPerProcessOption memoryPerProcess = (MemoryPerProcessOption)options.Where(o => o is MemoryPerProcessOption).LastOrDefault();
			if (memoryPerProcess != null)
			{
				p.StandardInput.WriteLine("#PBS -l pmem={0}gb", memoryPerProcess.MemoryPerProcessor);
			}

			if (dependencies.Count > 0)
			{
				// Use afterany instead of afterok? Start the job no matter whether PBS thinks the dependencies succeeded.
				List<string> after = new List<string>();
				foreach (Job dependency in dependencies)
				{
					// Skip if dependency has already finished (may apply if resubmitting a failed job).
					if (dependency.State == JobState.Waiting || dependency.State == JobState.Running)
					{
						after.Add(dependency.Id);
					}
				}
				if (after.Count > 0)
				{
					p.StandardInput.WriteLine("#PBS -W depend=afterany:{0}", string.Join(":", after));
				}
			}

			p.StandardInput.WriteLine();
			p.StandardInput.WriteLine("cd $PBS_O_WORKDIR");
			p.StandardInput.WriteLine("mono {0} --jobmanager '{1}' --start {2}", Assembly.GetEntryAssembly().Location, manager.OutputDirectory.FullName, index);
			p.StandardInput.Close();
			p.WaitForExit();

			if (p.ExitCode != 0)
			{
				throw new Exception(string.Format("Couldn't submit job. Error message: {0}", p.StandardError.ReadToEnd().Trim()));
			}

			// Read PBS-id from output.
			string id = p.StandardOutput.ReadToEnd().Trim();

			if (id.Contains("\n") || id.Contains("\r"))
			{
				// This shouldn't happen, but important for the integrity.
				throw new Exception("Invalid PBS id.");
			}

			return id;
		}

		private void PbsDelete()
		{
			PbsDelete(Id, false);
		}

		internal static void PbsDelete(string id, bool ignoreError)
		{
			Process p = new Process();
			p.StartInfo.FileName = "qdel";
			p.StartInfo.Arguments = string.Format("{0}", id);
			p.StartInfo.UseShellExecute = false;
			p.StartInfo.RedirectStandardOutput = true;
			p.StartInfo.RedirectStandardError = true;
			p.Start();
			p.WaitForExit();

			if (p.ExitCode != 0 && !ignoreError)
			{
				throw new Exception(string.Format("Couldn't delete job. Error message: {0}", p.StandardError.ReadToEnd().Trim()));
			}
		}
		
		private bool PbsStatus()
		{
			Process p = new Process();
			p.StartInfo.FileName = "qstat";
			p.StartInfo.Arguments = string.Format("{0}", Id);
			p.StartInfo.UseShellExecute = false;
			p.StartInfo.RedirectStandardOutput = true;
			p.StartInfo.RedirectStandardError = true;
			p.Start();
			p.WaitForExit();

			if (p.ExitCode != 0)
			{
				return false;
			}

			// Still waiting or running.
			return true;
		}

		/// <summary>
		/// Job summary. Override to add more information. This property is only read once.
		/// </summary>
		public virtual string Summary
		{
			get
			{
				return GetType().Name;
			}
		}

		/// <summary>
		/// Job submission options. Override to change if resubmitting.
		/// </summary>
		public virtual JobSubmitOptionCollection SubmitOptions
		{
			get
			{
				return options;
			}
		}

		/// <summary>
		/// Job submission resources. Override to change if resubmitting. NO NOT USE THIS ANYMORE.
		/// </summary>
		public virtual JobResources Resources
		{
			get
			{
				return resources;
			}
		}

		/// <summary>
		/// Job dependencies. These jobs are guaranteed to have finished (or failed) before this job is started.
		/// No need to check for failed jobs explicitly; this is automatically performed when the job result is requested.
		/// </summary>
		public ReadOnlyList<Job> Dependencies
		{
			get
			{
				return dependencies;
			}
		}

		/// <summary>
		/// The index of the job. May be used for saving output files specific to this job.
		/// </summary>
		public int Index
		{
			get
			{
				if (manager == null)
				{
					return -1;
				}

				return index;
			}
		}

		/// <summary>
		/// The PBS id of the job.
		/// </summary>
		public string Id
		{
			get
			{
				if (manager == null)
				{
					return null;
				}

				return manager.Status.Entries[this].Id;
			}
		}

		public JobState State
		{
			get
			{
				if (manager == null)
				{
					return JobState.Waiting;
				}

				return manager.Status.Entries[this].State;
			}
		}

		public DateTime? StartTime
		{
			get
			{
				if (manager == null)
				{
					return null;
				}

				return manager.Status.Entries[this].StartTime;
			}
		}

		public DateTime? EndTime
		{
			get
			{
				if (manager == null)
				{
					return null;
				}

				return manager.Status.Entries[this].EndTime;
			}
		}

		public TimeSpan? RunningTime
		{
			get
			{
				if (!EndTime.HasValue || !StartTime.HasValue)
				{
					return null;
				}

				return EndTime.Value.Subtract(StartTime.Value);
			}
		}

		/// <summary>
		/// The job manager responsible for the job.
		/// </summary>
		public JobManager Manager
		{
			get
			{
				return manager;
			}
		}

		public Exception InnerException
		{
			get
			{
				LoadResult();

				return innerResult.InnerException;
			}
		}

		protected JobBaseResult InnerResult
		{
			get
			{
				LoadResult();

				return innerResult;
			}
		}

		/// <summary>
		/// The directory assigned to output of the job (directory isn't created automatically).
		/// </summary>
		public DirectoryInfo OutputDirectory
		{
			get
			{
				if (manager == null)
				{
					return null;
				}

				return manager.OutputDirectory.AddDirectory(index.ToString());
			}
		}

		/// <summary>
		/// The file used to store the console output of the job.
		/// </summary>
		public FileInfo ConsoleOutputFile
		{
			get
			{
				if (manager == null)
				{
					return null;
				}

				return manager.OutputDirectory.AddFile("job.output." + index.ToString());
			}
		}

		/// <summary>
		/// The serialization file of the result (including any exceptions thrown).
		/// </summary>
		public FileInfo ResultFile
		{
			get
			{
				if (manager == null)
				{
					return null;
				}

				return manager.OutputDirectory.AddFile("job.result." + index.ToString() + ".gz");
			}
		}

		[Serializable]
		protected class JobBaseResult
		{
			private Exception innerException;

			public JobBaseResult()
				: this(null)
			{
				// Job ran succesfully.
			}

			public JobBaseResult(Exception innerException)
			{
				// Job failed. Store exception.
				this.innerException = innerException;
			}

			public Exception InnerException
			{
				get
				{
					return innerException;
				}
			}
		}
	}

	[Serializable]
	public abstract class Job<T> : Job
	{
		public Job(params Job[] dependencies)
			: this((IEnumerable<Job>)dependencies)
		{
		}

		public Job(IEnumerable<Job> dependencies)
			: base(dependencies)
		{
		}

		public Job(JobSubmitOptionCollection options, params Job[] dependencies)
			: this(options, (IEnumerable<Job>)dependencies)
		{
		}

		public Job(JobSubmitOptionCollection options, IEnumerable<Job> dependencies)
			: base(options, dependencies)
		{
		}

		public Job(JobResources resources, params Job[] dependencies)
			: this(resources, (IEnumerable<Job>)dependencies)
		{
		}

		public Job(JobResources resources, IEnumerable<Job> dependencies)
			: base(resources, dependencies)
		{
		}

		protected abstract T RunResult();

		protected override void Run()
		{
			// Not using this method.
		}

		protected override bool TryRun(out JobBaseResult result)
		{
			try
			{
				T r = RunResult();
				result = new JobResult(r);
			}
			catch (Exception e)
			{
				result = new JobResult(e);
				return false;
			}

			return true;
		}

		/// <summary>
		/// The result of the job. No need to check for failure explicitly; this is automatically performed when the job result is requested.
		/// </summary>
		public T Result
		{
			get
			{
				HandleException();

				return ((JobResult)InnerResult).Result;
			}
		}

		[Serializable]
		private class JobResult : JobBaseResult
		{
			private T result;

			public JobResult(T result)
				: base()
			{
				// Job ran succesfully. Store result.
				this.result = result;
			}

			public JobResult(Exception innerException)
				: base(innerException)
			{
				// Job failed. Store exception.
			}

			public T Result
			{
				get
				{
					return result;
				}
			}
		}
	}
}
